package fr.lelouet.monitoring.snapshot;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.lelouet.monitoring.snapshot.receivers.DirectoryWriter;
import fr.lelouet.monitoring.snapshot.receivers.LoggerReceiver;
import fr.lelouet.tools.Configurable;

/**
 * reads snapshots in a directory and send them to receivers.<br />
 * To use it, create it and configure it, then call {@link #run()} to start the
 * reading, and {@link #stop()} to stop reading snapshots in the directory if it
 * is launched in a Thread<br />
 * You can add receivers directly with {@link #register(SnapshotReceiver)} too<br />
 * It is not synchronized, so should not be shared. Any call to {@link #run()}
 * will clean every already loaded data and start reloading the directory
 */
public class DirectoryReader implements Configurable, Runnable {

	private static Logger logger = LoggerFactory
			.getLogger(DirectoryReader.class);

	/** the key for the file to observe to read modifications */
	public static final String DIRECTORY_KEY = "replay.directory";

	/** the key for the receivers to send snapshots to */
	public static final String RECEIVERS_KEY = "replay.receivers";

	public static final String RECEIVERS_DEFAULT = LoggerReceiver.class
			.getCanonicalName();

	/**
	 * the key to specify whether this should handle old snapshots(true), or
	 * discard them at first(false). default=false
	 */
	public static final String HANDLEOLDSNAPSHOT_KEY = "replay.useold";

	protected boolean handleOldSnapshots = false;

	/**
	 * specify if this should send old snapshots to the {@link SnapshotReceiver}
	 * s registered when {@link #run() starting a new monitoring}
	 */
	public void setHandleOldSnapshots(boolean handle) {
		handleOldSnapshots = handle;
	}

	public static final String LOOPDURATIONMS_KEY = "directoryReader.loopDuration";

	public static final long READINGLOOPDURATIONMS_DEFAULT = 500L;

	/** ms to wait between two checks on the directory */
	public long loopDuration = READINGLOOPDURATIONMS_DEFAULT;

	public void setLoopDuration(long ms) {
		loopDuration = ms;
	}

	private final List<SnapshotReceiver> receivers = new ArrayList<SnapshotReceiver>();

	/** adds an observer to notify of modifications */
	public void register(SnapshotReceiver obs) {
		synchronized (receivers) {
			receivers.add(obs);
		}
	}

	/** removes a registered observer */
	public void unregister(SnapshotReceiver obs) {
		synchronized (receivers) {
			receivers.remove(obs);
		}
	}

	protected String dirPath = "./";

	protected File dir;

	public void setDirPath(String dirPath) {
		this.dirPath = dirPath;
	}

	@Override
	public void configure(Properties prop) {

		setDirPath(prop.getProperty(DIRECTORY_KEY, "./"));

		setHandleOldSnapshots(Boolean.parseBoolean(prop.getProperty(
				HANDLEOLDSNAPSHOT_KEY, "false")));

		setLoopDuration(Long.parseLong(prop.getProperty(LOOPDURATIONMS_KEY,
				Long.toString(READINGLOOPDURATIONMS_DEFAULT))));

		receivers.clear();
		String receivers_prop = prop.getProperty(RECEIVERS_KEY,
				RECEIVERS_DEFAULT);
		String[] receivers_l = receivers_prop.split(" ");
		for (String receiverName : receivers_l) {
			if (receiverName.length() > 0) {
				try {
					SnapshotReceiver receiver = (SnapshotReceiver) Class
							.forName(receiverName).newInstance();
					register(receiver);
				} catch (Exception e) {
					logger.error(
							"could not add receiver from class {}. exception : {}",
							receiverName, e.toString());
				}
			}
		}
	}

	/**
	 * snapshots already added to this. This is useful for debugging
	 * informations, or to get the data once all the snapshots have been loaded
	 */
	protected final List<HVSnapshot> snapshots = new ArrayList<HVSnapshot>();

	protected final Set<String> readFiles = new HashSet<String>();

	/** @return a copy of the snapshots already read from the directory */
	public List<HVSnapshot> getSnapshots() {
		synchronized (snapshots) {
			return new ArrayList<HVSnapshot>(snapshots);
		}
	}

	/** set to true when the thread should stop observation */
	protected boolean hasToStop = false;

	/**
	 * clean it, then start following observing modifications in the specified
	 * directory
	 */
	@Override
	public void run() {
		reset();
		while (!hasToStop) {
			loadNewFiles();
			try {
				Thread.sleep(loopDuration);
			} catch (InterruptedException e) {
				logger.warn(e.toString());
			}
		}
	}

	protected final FileFilter newFilesFilter = new FileFilter() {
		@Override
		public boolean accept(File file) {
			return file.isFile()
					&& file.getName().endsWith(DirectoryWriter.FILE_EXTENSION)
					&& !readFiles.contains(file.getAbsolutePath());
		}
	};

	/**
	 * if set to true, the modifications in the directory must be sent to the
	 * {@link SnapshotReceiver}s registered. this should be set to false when
	 * the directory contains older data that are not wanted
	 */
	protected boolean notifyObservers = true;

	/**
	 * check new files and load them
	 * 
	 * @return the list of Snapshots that have been loaded from new files
	 */
	public List<HVSnapshot> loadNewFiles() {
		List<HVSnapshot> loadedSnapshots = new ArrayList<HVSnapshot>();
		if (dir == null) {
			dir = new File(dirPath);
		}
		if (!dir.exists()) {
			logger.info("directory {} seems not to exist anymore", dirPath);
			return new ArrayList<HVSnapshot>();
		}
		for (File child : dir.listFiles(newFilesFilter)) {
			readFiles.add(child.getAbsolutePath());
			try {
				InputStream stream = new FileInputStream(child);
				InputStream buffer = new BufferedInputStream(stream);
				ObjectInput input = new ObjectInputStream(buffer);
				HVSnapshot retrieved = (HVSnapshot) input.readObject();
				synchronized (snapshots) {
					snapshots.add(retrieved);
				}
				if (notifyObservers) {
					synchronized (receivers) {
						for (SnapshotReceiver obs : receivers) {
							obs.hypervisorModification(retrieved);
						}
					}
				}
			} catch (Exception e) {
				logger.error("while loading file " + child + ", "
						+ e.toString() + " ; " + e.getStackTrace()[0]);
			}
		}
		return loadedSnapshots;
	}

	/** if running in a thread, request this to stop */
	public void stop() {
		hasToStop = true;
	}

	/**
	 * removes any loaded data, and update the observed directory. Called before
	 * any {@link #run()}
	 */
	public void reset() {
		dir = new File(dirPath);
		readFiles.clear();
		snapshots.clear();
	}

	public static void main(String[] args) throws IOException {
		File f = new File(SharedMainConfig.snapshotsDir);
		if (!f.exists()) {
			System.err.println("no directory named snapshots");
		}
		File dir = null;
		for (File child : f.listFiles()) {
			if (child.isDirectory()) {
				dir = child;
				break;
			}
		}
		if (dir != null) {
			System.out.println("loading data from directory " + dir);
			DirectoryReader reader = new DirectoryReader();
			reader.register(new LoggerReceiver());
			reader.setDirPath(dir.getCanonicalPath());
			reader.run();
		} else {
			System.err.println("could not find a correct snapshots directory");
		}
	}
}
